package org.apache.ocean.main;

import java.io.File;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.search.DefaultSimilarity;
import org.apache.lucene.search.Similarity;
import org.apache.ocean.ClusterUtil;
import org.apache.ocean.analysis.TokenFilterFactory;
import org.apache.ocean.analysis.TokenizerChain;
import org.apache.ocean.analysis.TokenizerFactory;
import org.apache.ocean.main.fields.DateFieldType;
import org.apache.ocean.main.fields.DoubleFieldType;
import org.apache.ocean.main.fields.FieldType;
import org.apache.ocean.main.fields.LongFieldType;
import org.apache.ocean.main.fields.SchemaField;
import org.apache.ocean.main.fields.StringFieldType;
import org.apache.ocean.main.fields.SchemaField.Attribute;
import org.apache.ocean.main.util.Constants;
import org.apache.ocean.main.util.Timeout;
import org.apache.ocean.main.util.XMLUtil;
import org.apache.ocean.name.FileClient;
import org.jdom.Element;

/**
 * 
 * @author jasonr
 */
// TODO: verify copy source, and copy to exists
// TODO: test international analyzers
// TODO: remove fileevent listeners, change to just loaders
public class IndexSchema {
	public static Logger log = Logger.getLogger(IndexSchema.class.getName());
	private static final String[] packages = { "org.apache.ocean.main.fields", "org.apache.ocean.analysis" };
	private Date lastModifiedDate;
	private Map<String, Category> categoryMap = new HashMap<String, Category>();
	private Map<String, FieldType> fieldTypeMap = new HashMap<String, FieldType>();
	private List<FileClient.FileEventListener> fileEventListeners = new ArrayList<FileClient.FileEventListener>();
	private Similarity similarity = new DefaultSimilarity();
	private String xml;
	private String defaultSearchField;
	private Date lastModified;
	private Long version;

	public static interface Loader {
		public IndexSchema getIndexSchema(Timeout timeout) throws Exception;

		public IndexSchema getIndexSchema(long version, Timeout timeout) throws Exception;
	}

	public List<Category> getCategories() {
		return new ArrayList<Category>(categoryMap.values());
	}

	public long getVersion() {
		return version;
	}

	public Category getCategory(String name) {
		return categoryMap.get(name);
	}

	public static class Category {
		private Map<String, SchemaField> fieldMap = new HashMap<String, SchemaField>();
		private Map<String, CopyFields> copyFieldMap = new HashMap<String, CopyFields>(); // source
		private Analyzer analyzer;
		private Analyzer queryAnalyzer;
		private String name;
		private IndexSchema indexSchema;
		private Map<String, SchemaField> defaultFieldMap = new HashMap<String, SchemaField>();
		private String defaultSearchField;

		public Category(Element categoryElement, IndexSchema indexSchema) throws Exception {
			this.indexSchema = indexSchema;
			name = XMLUtil.getAttributeString("name", categoryElement);
			assert name != null;
			defaultSearchField = XMLUtil.getChildText("defaultsearchfield", categoryElement);
			assert defaultSearchField != null;
			Element fieldsElement = categoryElement.getChild("fields");
			for (Element fieldElement : XMLUtil.getChildren(fieldsElement)) {
				SchemaField schemaField = processField(fieldElement);
				addSchemaField(schemaField);
			}
			for (Element copyFieldElement : XMLUtil.getChildren("copyField", categoryElement)) {
				processCopyField(copyFieldElement);
			}
			analyzer = new OceanIndexAnalyzer(this);
			queryAnalyzer = new OceanQueryAnalyzer(this);
		} 
		
		public List<SchemaField> getFieldIndexables() {
			List<SchemaField> fields = new ArrayList<SchemaField>();
			for (SchemaField schemaField : fieldMap.values()) {
				if (schemaField.isFieldIndexable()) {
					fields.add(schemaField);
				}
			}
			return fields;
		}

		public List<String> getFieldIndexableNames() {
			List<String> fieldNames = new ArrayList<String>();
			for (SchemaField schemaField : fieldMap.values()) {
				if (schemaField.isFieldIndexable()) {
					fieldNames.add(schemaField.getName());
				}
			}
			return fieldNames;
		}

		public String getDefaultSearchFieldName() {
			return defaultSearchField;
		}

		public IndexSchema getIndexSchema() {
			return indexSchema;
		}

		public String getName() {
			return name;
		}

		public String indexedToReadable(String field, String indexed) {
			SchemaField schemaField = getField(field);
			if (schemaField == null)
				throw new IllegalArgumentException("unknown field: " + field);
			return schemaField.getFieldType().indexedToReadable(indexed);
		}

		public String readableToIndexed(String field, String readable) {
			SchemaField schemaField = getField(field);
			if (schemaField == null)
				throw new IllegalArgumentException("unknown field: " + field);
			return schemaField.getFieldType().toInternal(readable);
		}

		public boolean isDefaultField(String name) {
			return defaultFieldMap.containsKey(name);
		}

		protected SchemaField processField(Element element) throws Exception {
			String name = element.getAttributeValue("name");
			String type = element.getAttributeValue("type");
			FieldType fieldType = indexSchema.getFieldType(type);
			if (fieldType == null)
				throw new Exception("unknown field type: " + type);
			SchemaField schemaField = new SchemaField(name, fieldType);
			schemaField.fieldType = fieldType;
			for (org.jdom.Attribute attr : XMLUtil.getAttributes(element)) {
				String attrName = attr.getName();
				if (StringUtils.equals("true", attr.getValue())) {
				  Attribute attribute = SchemaField.attributeMap.get(attrName);
				  schemaField.addAttribute(attribute);
				}
			}
			
			if (schemaField.name == null)
				throw new Exception("name null " + XMLUtil.printAndDetach(element));
			return schemaField;
		}

		public Analyzer getAnalyzer() {
			return analyzer;
		}

		public Analyzer getQueryAnalyzer() {
			return queryAnalyzer;
		}
    
		public FieldType getFieldType(String name) {
		  return fieldMap.get(name).getFieldType();
		}
		
		public SchemaField getField(String name) {
			return fieldMap.get(name);
		}

		private void exceptionIfFieldUnknown(String fieldName) {
			if (!fieldMap.containsKey(fieldName)) {
				throw new RuntimeException("unknown field: " + fieldName);
			}
		}

		public void addSchemaField(SchemaField schemaField) throws Exception {
			if (fieldMap.containsKey(schemaField.name))
				throw new Exception("schemaField " + schemaField.name + " already defined");
			fieldMap.put(schemaField.name, schemaField);
		}

		private void addDefaultFieldType(String name, String type) throws Exception {
			addDefaultFieldType(name, type, true);
		}

		private void addDefaultFieldType(String name, String type, boolean storeAsPayload) throws Exception {
			FieldType fieldType = indexSchema.getFieldType(type);
			SchemaField schemaField = new SchemaField(name, fieldType);
			schemaField.addAttribute(Attribute.INDEXED);
			schemaField.addAttribute(Attribute.STORED);
			schemaField.addAttribute(Attribute.SORTMISSINGLAST);
			if (storeAsPayload)
				schemaField.addAttribute(Attribute.PAYLOAD);
			addSchemaField(schemaField);
			defaultFieldMap.put(name, schemaField);
		}

		protected void processCopyField(Element element) throws Exception {
			String source = element.getAttributeValue("source");
			if (!fieldMap.containsKey(source)) {
				throw new Exception("unknown source: " + source);
			}
			CopyFields copyFields = copyFieldMap.get(source);
			if (copyFields == null) {
				copyFields = new CopyFields(source);
				copyFieldMap.put(source, copyFields);
			}
			String destText = element.getAttributeValue("dest");
			for (String dest : ClusterUtil.split(destText)) {
				if (isDefaultField(dest))
					throw new Exception("cannot copy to default field " + dest);
				if (!fieldMap.containsKey(dest)) {
					throw new Exception("unknown dest: " + dest);
				}
				copyFields.addTo(dest);
			}
		}

		private void addDefaultFieldTypes() throws Exception {
			addDefaultFieldType(Constants.ID, "long");
			addDefaultFieldType(Constants.SNAPSHOTID, "long");
			addDefaultFieldType(Constants.SCHEMAVERSION, "long");
			addDefaultFieldType(Constants.LATITUDE, "double");
			addDefaultFieldType(Constants.LONGITUDE, "double");
			addDefaultFieldType(Constants.DISTANCE, "double");
			addDefaultFieldType(Constants.SCORE, "double");
		}

		public List<String> getUserFieldNames() {
			Set<String> set = new HashSet<String>();
			set.addAll(fieldMap.keySet());
			set.removeAll(defaultFieldMap.keySet());
			set.removeAll(copyFieldMap.keySet());
			return new ArrayList<String>(set);
		}
	}

	public IndexSchema(String xml) throws Exception {
		this.xml = xml;
		Element element = XMLUtil.parseElement(xml);
		init(element);
	}

	public IndexSchema(File schemaFile) throws Exception {
		xml = FileUtils.readFileToString(schemaFile, "UTF-8");
		lastModified = new Date(schemaFile.lastModified());
		Element element = XMLUtil.parseElement(xml);
		init(element);
	}

	private Object newInstance(String name) throws Exception {
		return ClusterUtil.findClass(name, packages).newInstance();
	}

	private void init(Element rootElement) throws Exception {
		version = XMLUtil.getAttributeLong("version", rootElement);
		if (version == null)
			throw new Exception("version null");
		defaultSearchField = rootElement.getChildText("defaultSearchField");
		Element typesElement = rootElement.getChild("types");
		addFieldType(new DateFieldType());
		addFieldType(new LongFieldType());
		addFieldType(new DoubleFieldType());
		addFieldType(new StringFieldType());
		for (Element typeElement : XMLUtil.getChildren(typesElement)) {
			FieldType fieldType = processFieldType(typeElement);
			addFieldType(fieldType);
		}
		for (Element categoryElement : XMLUtil.getChildren("category", rootElement)) {
			Category category = new Category(categoryElement, this);
			categoryMap.put(category.name, category);
		}
	}

	private void addFieldType(FieldType fieldType) throws Exception {
		if (fieldTypeMap.containsKey(fieldType.getName()))
			throw new Exception(fieldType.getName() + " exists already");
		fieldTypeMap.put(fieldType.getName(), fieldType);
	}

	public Similarity getSimilarity() {
		return similarity;
	}

	public String toXML() {
		return xml;
	}

	public String getDefaultSearchFieldName() {
		return defaultSearchField;
	}

	protected FieldType processFieldType(Element fieldTypeElement) throws Exception {
		String clazz = fieldTypeElement.getAttributeValue("class");
		String name = fieldTypeElement.getAttributeValue("name");
		FieldType fieldType = createFieldType(clazz);
		fieldType.setTypeName(name);
		for (Element analyzerElement : XMLUtil.getChildren("analyzer", fieldTypeElement)) {
			String type = analyzerElement.getAttributeValue("type");
			Analyzer analyzer = createAnalyzer(analyzerElement);
			if (StringUtils.isBlank(type)) {
				fieldType.setAnalyzer(analyzer);
			} else if (StringUtils.equals("query", type)) {
				fieldType.setQueryAnalyzer(analyzer);
			} else if (StringUtils.equals("index", type)) {
				fieldType.setAnalyzer(analyzer);
			}
		}
		return fieldType;
	}

	protected Analyzer createAnalyzer(Element analyzerElement) throws Exception {
		String analyzerClassName = analyzerElement.getAttributeValue("class");
		if (StringUtils.isNotBlank(analyzerClassName)) {
			return (Analyzer) newInstance(analyzerClassName);
		} else {
			Element tokenizerElement = analyzerElement.getChild("tokenizer");
			TokenizerFactory tokenizerFactory = createTokenizerFactory(tokenizerElement);
			List<TokenFilterFactory> tokenFilterFactories = new ArrayList<TokenFilterFactory>();
			for (Element filterElement : XMLUtil.getChildren("filter", analyzerElement)) {
				TokenFilterFactory tokenFilterFactory = createTokenFilterFactory(filterElement);
				if (tokenFilterFactory instanceof FileClient.FileEventListener) {
					this.fileEventListeners.add((FileClient.FileEventListener) tokenFilterFactory);
				}
				tokenFilterFactories.add(tokenFilterFactory);
			}
			return new TokenizerChain(tokenizerFactory, tokenFilterFactories.toArray(new TokenFilterFactory[tokenFilterFactories.size()]));
		}
	}

	protected TokenizerFactory createTokenizerFactory(Element tokenizerElement) throws Exception {
		String clazz = tokenizerElement.getAttributeValue("class");
		TokenizerFactory tokenizerFactory = (TokenizerFactory) newInstance(clazz);
		Map<String, String> args = ClusterUtil.toMapExcept(tokenizerElement, "class");
		tokenizerFactory.init(args);
		return tokenizerFactory;
	}

	protected TokenFilterFactory createTokenFilterFactory(Element filterElement) throws Exception {
		String clazz = filterElement.getAttributeValue("class");
		TokenFilterFactory tokenFilterFactory = (TokenFilterFactory) newInstance(clazz);
		Map<String, String> args = ClusterUtil.toMapExcept(filterElement, "class");
		tokenFilterFactory.init(args);
		return tokenFilterFactory;
	}

	protected FieldType createFieldType(String className) throws Exception {
		return (FieldType) newInstance(className);
	}

	public FieldType getFieldType(String name) {
		return fieldTypeMap.get(name);
	}

	public static class CopyFields {
		public String from;
		public Set<String> tos = new HashSet<String>();

		public CopyFields(String from) {
			this.from = from;
		}

		public void addTo(String to) {
			tos.add(to);
		}

		public String toString() {
			return ReflectionToStringBuilder.toString(this);
		}
	}

	public static class OceanIndexAnalyzer extends Analyzer {
		protected final Map<String, Analyzer> analyzerMap = new HashMap<String, Analyzer>();
		protected Category category;

		public OceanIndexAnalyzer(Category category) {
			this.category = category;
			for (SchemaField schemaField : category.fieldMap.values()) {
				FieldType fieldType = schemaField.getFieldType();
				if (fieldType == null)
					throw new RuntimeException("fieldType null for " + schemaField);
				Analyzer analyzer = schemaField.getAnalyzer();
				if (analyzer == null)
					analyzer = fieldType.getAnalyzer();
				analyzerMap.put(schemaField.getName(), analyzer);
			}
		}

		protected Analyzer getAnalyzer(String fieldName) {
			category.exceptionIfFieldUnknown(fieldName);
			Analyzer analyzer = analyzerMap.get(fieldName);
			if (analyzer == null)
				throw new RuntimeException("no analyzer for field: " + fieldName);
			return analyzer;
		}

		public TokenStream tokenStream(String fieldName, Reader reader) {
			category.exceptionIfFieldUnknown(fieldName);
			return getAnalyzer(fieldName).tokenStream(fieldName, reader);
		}

		public int getPositionIncrementGap(String fieldName) {
			return getAnalyzer(fieldName).getPositionIncrementGap(fieldName);
		}
	}

	private static class OceanQueryAnalyzer extends OceanIndexAnalyzer {
		public OceanQueryAnalyzer(Category category) {
			super(category);
			for (SchemaField schemaField : category.fieldMap.values()) {
				Analyzer analyzer = schemaField.getFieldType().getQueryAnalyzer();
				analyzerMap.put(schemaField.getName(), analyzer);
			}
		}

		protected Analyzer getAnalyzer(String fieldName) {
			category.exceptionIfFieldUnknown(fieldName);
			Analyzer analyzer = analyzerMap.get(fieldName);
			return analyzer;
		}
	}
}
